<?php
/**
 * @package PHPRandom
 * @author Eric Guerin <1fizzle@gmail.com>
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 * @version 0.1
 *
 * This script is meant to generate true random numbers using the 
 * publically accessible API at http://random.org
 * all bug reports or other info please send
 * to 1fizzle@gmail.com
 *
 * Requires basic installation of PHP cURL library
 * @link http://php.net/curl/
 * If cURL is unavailable, set $use_curl = false
 * Doing this will attempt to use the built-in PHP function
 * file_get_contents(), but it is NOT recommended
 * as it does not handle errors and is often unavailable on
 * production servers (the php.ini setting allow_url_fopen impacts
 * the use of file_get_contents(), for example)
 * 
 * Hopefully everything in this script conforms to
 * the requests of the makers/maintainers of
 * http://random.org, if it doesn't, or if they
 * implement new requirements, my bad. Let me know
 * and I'll try and rectify the situation the best I can.
 */
 
class Random {
	/**
	 * All configuration options happen between here and when
	 * told to stop configuring. Hopefully the documentation contained
	 * within is enough to know what the config options are for
	 */
	
	/**
	* @var string URL of random.org API
	* Will be appended with one of integers|sequences|strings
	* when each of those methods are called
	*/
	private $api_url = 'http://www.random.org/';
	
	/**
	 * @var bool whether or not to make use of PHPs cURL library
	 * set to false if you are having issues with 
	 * the curl_init() function when running this script
	 */ 
	private $use_curl = true;
	
	/**
	 * @var string the additional URL for integer requests
	 */
	private $int_api_url = 'integers/?';
	
	/**
	 * @var string the additional URL for integer sequence requests
	 */
	private $seq_api_url = 'sequences/?';
	
	/**
	 * @var string the additional URL for string requests
	 */
	private $string_api_url = 'strings/?';
	
	/**
	 * @var string URL of the random.org API quota checker
	 * Used to check how many requests are left for the machine
	 * that makes the request
	 */
	private $quota_url = 'http://www.random.org/quota/?format=plain';
	
	/**
	 * @var bool whether or not to check against the API quota
	 * when creating requests for randomness
	 */
	public $check_quota = true;
	 
	/** 
	 * @var int The number of integers requested, expects 1 - 1e4 
	 */
	public $int_num = 1;
	
	/**
	 * @var int The smallest value allowed for each integer, expects -1e9 - 1e9 
	 */
	public $int_min = 0;
	
	/** 
	 * @var int The largest value allowed for each integer, expects -1e9 - 1e9
	 */
	public $int_max = 0;
	
	/**
	 * @var int The number of columns in which the integers
	 * will be arranged. The integers should be read 
	 * (or processed) left to right across columns, expects 1-1e9
	 */
	public $int_col = 1;
	
	/**
	* @var int The base that will be used to print the numbers, 
	* i.e., binary, octal, decimal or hexadecimal, respectively
	* 2=binary, 8=octal, 10=decimal, 16=hexadecimal, 
	* expects 2 | 8 | 10 | 16
	*/
	public $int_base = 10;
	
	/**
	* @var string Determines the return type of the document 
	* that the server produces as its response. If html is specified, 
	* the server produces a nicely formatted XHTML document 
	* (MIME type text/html), which will display well in a browser 
	* but which is somewhat cumbersome to parse. If plain is specified, 
	* the server produces as minimalistic document of type plain text 
	* (MIME type text/plain) document, which is easy to parse. 
	* Expects html | plain
	*/
	public $int_format = 'plain';
	
	/**
	* @var string Determines the randomization to use to 
	* generate the numbers. If new is specified, then a new 
	* randomization will created from the truly random bitstream at 
	* RANDOM.ORG. This is probably what you want in most cases. 
	* If id.identifier is specified, the identifier is used to 
	* determine the randomization in a deterministic fashion from a 
	* large pool of pregenerated random bits. Because the numbers are 
	* produced in a deterministic fashion, specifying an id basically 
	* uses RANDOM.ORG as a pseudo-random number generator. 
	* The third (date.iso-date) form is similar to the second; 
	* it allows the randomization to be based on one of the daily 
	* pregenerated files. This form must refer to one of the dates 
	* for which files exist, so it must be the current day (according to UTC) 
	* or a day in the past. The date must be in ISO 8601 format (i.e., YYYY-MM-DD) 
	* or one of the two shorthand strings today or yesterday.
	* Expects new | id.identifier | date.iso-date
	*/
	public $int_rand = 'new';	
	
	/**
	 * @var int The lower bound of the interval (inclusive)
	 * expects -1e9 - 1e9
	 */
	public $seq_min = 0;
	
	/**
	 * @var int the upper bound of the interval (inclusive)
	 * expects -1e9 - 1e9
	 */
	public $seq_max = 0;
	
	/**
	 * @var int The number of columns in which the integers will be arranged. The integers should be read (or processed) left to right across columns
	 * expects 1 - 1e9
	 */
	public $seq_col = 1;
	
	/**
     * @var string Determines the return type of the document 
	 * that the server produces as its response. If html is specified, 
	 * the server produces a nicely formatted XHTML document 
	 * (MIME type text/html), which will display well in a browser 
	 * but which is somewhat cumbersome to parse. If plain is specified, 
	 * the server produces as minimalistic document of type plain text 
	 * (MIME type text/plain) document, which is easy to parse. 
	 * Expects html | plain
	 */
	public $seq_format = 'plain';
	
	/**
	 * @var string Determines the randomization to use to 
	 * generate the numbers. If new is specified, then a new 
	 * randomization will created from the truly random bitstream at 
	 * RANDOM.ORG. This is probably what you want in most cases. 
	 * If id.identifier is specified, the identifier is used to 
	 * determine the randomization in a deterministic fashion from a 
	 * large pool of pregenerated random bits. Because the numbers are 
	 * produced in a deterministic fashion, specifying an id basically 
	 * uses RANDOM.ORG as a pseudo-random number generator. 
	 * The third (date.iso-date) form is similar to the second; 
	 * it allows the randomization to be based on one of the daily 
	 * pregenerated files. This form must refer to one of the dates 
	 * for which files exist, so it must be the current day (according to UTC) 
	 * or a day in the past. The date must be in ISO 8601 format (i.e., YYYY-MM-DD) 
	 * or one of the two shorthand strings today or yesterday.
	 * Expects new | id.identifier | date.iso-date
	 */
	public $seq_rand = 'new';
	
	/**
	 * @var int the number of strings requested, expects 1 - 1e4
	 */
	public $string_num = 1;
	
	/**
	 * @var int The length of the strings. All the 
	 * strings produced will have the same length.
	 * expects 1 - 20
	 */
	public $string_length = 1;
	
	/**
	 * @var string whether or not to include digits in the
	 * random string generation. expects on | off
	 */
	public $string_digits = 'on';
	
	/**
	 * @var string whether or not to include uppercase alphabetic
	 * characters (A-Z) in the random string generation
	 * expects on | off
	 */
	public $string_upperalpha = 'on';
	
	/**
	 * @var string whether or not ot include lowercase alphabetic
	 * characters (a-z) in the random string generation
	 * expects on | off
	 */
	public $string_loweralpha = 'on';
	
	/**
	 * @var string Determines whether the strings picked 
	 * should be unique (as a series of raffle tickets drawn 
	 * from a hat) or not (as a series of die rolls). If unique 
	 * is set to on, then there is the additional constraint that 
	 * the number of strings requested (num) must be less than or 
	 * equal to the number of strings that exist with the selected 
	 * length and characters. expects on | off
	 */
	public $string_unique = 'off';
	
	/**
     * @var string Determines the return type of the document 
	 * that the server produces as its response. If html is specified, 
	 * the server produces a nicely formatted XHTML document 
	 * (MIME type text/html), which will display well in a browser 
	 * but which is somewhat cumbersome to parse. If plain is specified, 
	 * the server produces as minimalistic document of type plain text 
	 * (MIME type text/plain) document, which is easy to parse. 
	 * Expects html | plain
	 */
	public $string_format = 'plain';
	
	/**
	 * @var string Determines the randomization to use to 
	 * generate the numbers. If new is specified, then a new 
	 * randomization will created from the truly random bitstream at 
	 * RANDOM.ORG. This is probably what you want in most cases. 
	 * If id.identifier is specified, the identifier is used to 
	 * determine the randomization in a deterministic fashion from a 
	 * large pool of pregenerated random bits. Because the numbers are 
	 * produced in a deterministic fashion, specifying an id basically 
	 * uses RANDOM.ORG as a pseudo-random number generator. 
	 * The third (date.iso-date) form is similar to the second; 
	 * it allows the randomization to be based on one of the daily 
	 * pregenerated files. This form must refer to one of the dates 
	 * for which files exist, so it must be the current day (according to UTC) 
	 * or a day in the past. The date must be in ISO 8601 format (i.e., YYYY-MM-DD) 
	 * or one of the two shorthand strings today or yesterday.
	 * Expects new | id.identifier | date.iso-date
	 */
	public $string_random = 'new';
	
	/**
	 ********** NO MORE CONFIGURATION OPTIONS **********
	 * Any modifications to the code below are at your
	 * own risk, and assume you know what you're doing
	 */
	 
	/**
	 * @var array of possible HTTP response codes and their descriptions
	 */
	private $http_codes = array(
		100	=>	'100 Continue',
		101	=>	'101 Switching Protocols',
		200	=>	'200 OK',
		201	=>	'201 Created',
		202	=>	'202 Accepted',
		203	=>	'203 Non-Authoritative Information',
		204	=>	'204 No Content',
		205	=>	'205 Reset Content',
		206	=>	'206 Partial Content',
		300	=>	'300 Multiple Choices',
		301	=>	'301 Moved Permanently',
		302	=>	'302 Found',
		303	=>	'303 See Other',
		304	=>	'304 Not Modified',
		305	=>	'305 Use Proxy',
		306	=>	'306 (Unused)',
		307	=>	'307 Temporary Redirect',
		400	=>	'400 Bad Request',
		401	=>	'401 Unauthorized',
		402	=>	'402 Payment Required',
		403	=>	'403 Forbidden',
		404	=>	'404 Not Found',
		405	=>	'405 Method Not Allowed',
		406	=>	'406 Not Acceptable',
		407	=>	'407 Proxy Authentication Required',
		408	=>	'408 Request Timeout',
		409	=>	'409 Conflict',
		410	=>	'410 Gone',
		411	=>	'411 Length Required',
		412	=>	'412 Precondition Failed',
		413	=>	'413 Request Entity Too Large',
		414	=>	'414 Request-URI Too Long',
		415	=>	'415 Unsupported Media Type',
		416	=>	'416 Requested Range Not Satisfiable',
		417	=>	'417 Expectation Failed',
		500	=>	'500 Internal Server Error',
		501	=>	'501 Not Implemented',
		502	=>	'502 Bad Gateway',
		503	=>	'503 Service Unavailable',
		504	=>	'504 Gateway Timeout',
		505	=>	'505 HTTP Version Not Supported'
	);
	
	/**
	 * generates a given number of true random integers
	 */
	public function randInteger() {
		//set the correct API URL
		$api_url = $this->api_url . $this->int_api_url;
		
		//build URL parts
		$parts = array(
			'num' 		=> $this->int_num,
			'min'		=> $this->int_min,
			'max'		=> $this->int_max,
			'col'		=> $this->int_col,
			'base'		=> $this->int_base,
			'format'	=> $this->int_format,
			'rnd'		=> $this->int_rand
			);
			
		if($this->checkURLParts('int', $parts) && $this->checkQuota()) {
			$api_url .= http_build_query($parts);
			$ret = $this->httpGet($api_url);
		} else {
			exit('Error: Correct data has not been set for the API querystring');
		}
		
		return $ret;
	}
	
	/**
	 * generates true random integers in a given 
	 * sequence of numbers between min and max
	 * Note: This cannot exceed 10,000 numbers!
	 */
	public function randSequence() {
		//set the correct API URL
		$api_url = $this->api_url . $this->seq_api_url;
		
		//build URL parts
		$parts = array(
			'min'		=> $this->seq_min,
			'max'		=> $this->seq_max,
			'col'		=> $this->seq_col,
			'format'	=> $this->seq_format,
			'rnd'		=> $this->seq_rand
			);
			
		if($this->checkURLParts('seq', $parts) && $this->checkQuota()) {
			$api_url .= http_build_query($parts);
			$ret = $this->httpGet($api_url);
		} else {
			exit('Error: Correct data has not been set for the API querystring');
		}
		
		return $ret;
	}
	
	/**
	 * generates a true random string
	 */	
	public function randString() {
		//set the correct API URL
		$api_url = $this->api_url . $this->string_api_url;
		
		//build URL parts
		$parts = array(
			'num'			=> $this->string_num,
			'len'			=> $this->string_length,
			'digits'		=> $this->string_digits,
			'upperalpha'	=> $this->string_upperalpha,
			'loweralpha'	=> $this->string_loweralpha,
			'unique'		=> $this->string_unique,
			'format'		=> $this->string_format,
			'rnd'			=> $this->string_random
			);
			
		if($this->checkURLParts('seq', $parts) && $this->checkQuota()) {
			$api_url .= http_build_query($parts);
			$ret = $this->httpGet($api_url);
		} else {
			exit('Error: Correct data has not been set for the API querystring');
		}
		
		return $ret;
	}
	
	/**
	 * @param string $url the base URL of the API
	 */
	private function httpGet($url) {
		if($this->use_curl === true) {
			$ch = curl_init($url);
			curl_setopt($ch, CURLOPT_HEADER, false);
			curl_setopt($ch,  CURLOPT_RETURNTRANSFER, true);
			$ret = curl_exec($ch);
			$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
			curl_close($ch);

			//if the request returned anything but a 200 status code, display error
			if($this->checkStatus($http_status) !== true) {
				if(array_key_exists($http_status, $this->http_codes)) {
					$ret = 'Error: Request to API failed with Code: ' . $this->http_codes[$http_status];
				} else {
					$ret = 'Error: Request to API failed with HTTP Status Code: ' . $http_status;
				}
			}
		} else {
			$ret = array('return' => file_get_contents($url));
		}
		
		return $ret;
	}
	
	/**
	 * Checks against the API to make sure the IP address
	 * of the originating request has not exceeded the given
	 * quota set by the API
	 */
	private function checkQuota() {
		$ret = true;
		
		if($this->check_quota === true) {
			$quota = $this->httpGet($this->quota_url);
			if($quota <= 0) { //quota is used up
				$ret = false;	
			}
		}
		
		return $ret;
	}
			
	/**
	 * checks to make sure none of the properties are blank
	 * @param string $type type of URLParts to check, expects
	 * int|sequence|string
	 * @param array $parts array of key=>value URL parts
	 * @todo add checks for valid/expected data, not just empty/null
	 */
	private function checkURLParts($type, $parts) {
		$ret = true;
		if(is_array($parts)) {
			foreach($parts as $k => $v) {
				if(!isset($k) || !isset($v)) {
					$ret = false;
				}
			}
		} else $ret = false;
		
		return $ret;
	}
	
	/**
	 * @param string $http_status HTTP status code returned by API
	 * Only used if $this->use_curl is set to true
	 * @todo Add some more verbose error handling, perhaps.
	 */
	private function checkStatus($http_status) {
		if($http_status == 200)
			return true;
		else
			return $http_status;
	}
}
?>